﻿
Imports System.Net
Imports System.IO
Imports System.IO.Compression

Public Class httpProxy
    Public Const defaultRetryTimes As Int64 = 4
    Public Const defaultHoldtime As Int64 = 30000
    Public Const defaultSleeptime As Int64 = 100
    Public Const MIN_GZIP_SIZE As Int64 = 1024

    Protected context As httpContext = Nothing
    Protected request As HttpWebRequest = Nothing
    Protected response As HttpWebResponse = Nothing
    Private out_match As String = Nothing   'start from /
    Private in_match As String = Nothing    'full url
    Private in_requestPathquery As String = Nothing

    Private retrytimes As Int64
    Private holdtime As Int64
    Private sleeptime As Int64

    Public Sub initial(Optional ByVal retrytimes As Int64 = defaultRetryTimes, _
                       Optional ByVal holdtime As Int64 = defaultHoldtime, _
                       Optional ByVal sleeptime As Int64 = defaultSleeptime)
        Me.retrytimes = retrytimes
        Me.holdtime = holdtime
        Me.sleeptime = sleeptime
    End Sub

    Public Sub release()
        If Not context Is Nothing AndAlso Not context.closed() Then
            context.close()
        End If
        If Not response Is Nothing Then
            closestream(response.GetResponseStream())
            response.Close()
        End If
    End Sub

    Public Function proxy(ByVal ctx As httpContext, Optional ByRef e As Exception = Nothing) As Boolean
        Return proxy(ctx, ctx.url().pathquery, ctx.url().full, e)
    End Function

    Public Function proxy(ByVal ctx As httpContext, ByVal out_match As String, ByVal in_match As String, _
                          Optional ByRef e As Exception = Nothing) As Boolean
        Try
            context = ctx
            copy(Me.out_match, out_match)
            copy(Me.in_match, in_match)

            in_requestPathquery = strmid(context.url().pathquery, strlen(out_match))
            request = Nothing
            response = Nothing

            Return create() AndAlso syncRequestBody() AndAlso getresponse(e) AndAlso postresponse()
        Catch ex As Exception
            raiseError("caught unhandled exception for request " + ctx.url().full + ", ex " + ex.Message + _
                       ", call stack " + ex.StackTrace, errorHandle.errorType.exclamation)
            'release the request / reponse / context if caught exception,
            'otherwise, the code path will confirm them to be released properly
            release()
            Return False
        End Try
    End Function

    Protected Overridable Function create() As Boolean
        If debugassert(Not context Is Nothing, "do not have a valid httpContext input.") Then
            Try
                request = HttpWebRequest.Create(in_match + in_requestPathquery)
            Catch
                raiseError("cannot create HttpWebRequest, " + in_match + in_requestPathquery, _
                           errorHandle.errorType.exclamation)
                Return False
            End Try

            request.Headers().Clear()
            request.Method = context.request().HttpMethod
            copyHttpRequestHeaders(context.request(), request)
            'to see whether webrequest can use the same tcp connection in different instances.
            'request.KeepAlive = True
            'request.Connection = network.http.headers.connection.keep_alive
            'do not allow auto redirect, each http server will handle it, and FD deals it with following logic
            request.AllowAutoRedirect = False
#If CANNOTDETERMINE Then
            request.Proxy = WebRequest.DefaultWebProxy()
#End If
            Return True
        Else
            Return False
        End If
    End Function

    Private Function syncRequestBody() As Boolean
        If debugassert(Not context Is Nothing AndAlso Not request Is Nothing, _
                       "do not have a valid httpContext / HttpWebRequest input.") Then
            If context.request().HasEntityBody Then
                Dim requestStream As Stream = Nothing
                requestStream = request.GetRequestStream()
                If syncstream(context.request.InputStream, requestStream, _
                              length:=context.request().ContentLength64, _
                              retrytimes:=retrytimes, holdtime:=holdtime, _
                              sleeptime:=sleeptime) <> context.request().ContentLength64 Then
                    Return False
                Else
                    closestream(requestStream)
                End If
            End If

            Return True
        Else
            Return False
        End If
    End Function

    Private Function getresponse(Optional ByRef e As Exception = Nothing) As Boolean
        If debugassert(Not request Is Nothing, "do not have a valid HttpWebRequest input.") Then
            request.Timeout = holdtime
            Try
                response = request.GetResponse()
                Return True
            Catch ex As WebException
                'post the webexception to browser, ex, authority
                response = convertor.cast(Of HttpWebResponse)(ex.Response(), False)
                'do not retry if remote server responsed already
                If response Is Nothing Then
                    copy(e, ex)
                    Return False
                Else
                    Return True
                End If
            Catch ex As Exception
                copy(e, ex)
                Return False
            End Try
        Else
            Return False
        End If
    End Function

    Protected Overridable Sub generateResponseHeaders()
        context.response().Headers.Clear()
        copyHttpResponseHeaders(response, context.response())
        context.setstatuscode(response.StatusCode, response.StatusDescription)
    End Sub

    Private Sub redirect()
        If response.ResponseUri.Host = request.RequestUri.Host Then
            Dim responsePart As String = Nothing
            responsePart = strleft(context.url().full, _
                                   strlen(context.request.Url.ToString) - strlen(in_requestPathquery))
            If strendwith(responsePart, network.pathSeperator) AndAlso _
               strstartwith(response.ResponseUri.PathAndQuery, network.pathSeperator) Then
                strrmv(responsePart, strlen(responsePart) - strlen(network.pathSeperator))
            End If
            context.response.Redirect(responsePart + response.ResponseUri.PathAndQuery)
        Else
            context.response.Redirect(response.ResponseUri.AbsoluteUri())
        End If
    End Sub

    Private Shared Function enableGzip(ByVal contentType As String) As Boolean
        Return (strstartwith(contentType, "text", False) OrElse _
                strsame(contentType, network.http.contentType.applicationOctetStream) OrElse _
                strsame(contentType, network.http.contentType.rssxml))
    End Function

    Protected Overridable Function enableGzip() As Boolean
        Return context.acceptGzip() AndAlso _
               (Not contentLengthValid(response) OrElse response.ContentLength > MIN_GZIP_SIZE) AndAlso _
               enableGzip(response.ContentType)
    End Function

    Private Sub syncResponseBody()
        Dim s As Stream = Nothing
        Try
            s = response.GetResponseStream()
        Catch
        End Try
        If Not s Is Nothing Then
            Dim dest As Stream = Nothing
            Dim gzipStream As Stream = Nothing
            If enableGzip() Then
                context.buff().Position() = 0
                context.buff().SetLength(0)
                gzipStream = New GZipStream(context.buff(), CompressionMode.Compress, True)
                context.setcontentLength64(0)
                modifyheader(context.response().Headers, network.http.headers.ContentEncoding, network.gzipCompress)
                dest = gzipStream
            ElseIf contentLengthValid(response) Then
                'if contentLength is valid, context.response().ContentLength64() should be set already.
                dest = context.response().OutputStream()
            Else
                dest = context.buff()
            End If
            Dim synclen As Int64
            synclen = syncstream(s, dest, _
                                 length:=ifoper(contentLengthValid(response), response.ContentLength, npos), _
                                 retrytimes:=retrytimes, _
                                 holdtime:=holdtime, sleeptime:=sleeptime)
            If contentLengthValid(response) AndAlso _
               synclen <> response.ContentLength Then
                raiseError("cannot syncstream to request " + context.url.full, _
                           errorHandle.errorType.warning)
                'still return true here, as the http context is out of usage now.
            End If
            closestream(gzipStream)
        End If
    End Sub

    Private Function postresponse() As Boolean
        If debugassert(Not context Is Nothing AndAlso Not request Is Nothing AndAlso Not response Is Nothing, _
                       "do not have a valid httpContext or HttpWebRequest or HttpWebResponse input.") Then
            If response.ResponseUri <> request.RequestUri Then
                redirect()
            Else
                generateResponseHeaders()
                syncResponseBody()
            End If
            response.Close()
            Return True
        Else
            Return False
        End If
    End Function

    Public Sub New()
        initial()
    End Sub
End Class

Public Class httpProxyPool_t
    Inherits pool(Of httpProxy, _true, _false)

    Protected Overrides Function create() As httpProxy
        Return New httpProxy()
    End Function

    Protected Overrides Sub unalloc(ByRef inst As httpProxy)
        If Not inst Is Nothing Then
            inst.release()
        End If
        MyBase.unalloc(inst)
    End Sub
End Class